/*
    The MIT License (MIT)

    Copyright (c) 2015 Andreas Marek and Contributors

    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
    (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge,
    publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do
    so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
    LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
    CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.intellij.lang.jsgraphql.types.schema;

import com.google.common.collect.ImmutableList;
import com.intellij.lang.jsgraphql.types.AssertException;
import com.intellij.lang.jsgraphql.types.DirectivesUtil;
import com.intellij.lang.jsgraphql.types.Internal;
import com.intellij.lang.jsgraphql.types.PublicApi;
import com.intellij.lang.jsgraphql.types.language.InterfaceTypeDefinition;
import com.intellij.lang.jsgraphql.types.language.InterfaceTypeExtensionDefinition;
import com.intellij.lang.jsgraphql.types.util.TraversalControl;
import com.intellij.lang.jsgraphql.types.util.TraverserContext;

import java.util.*;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;

import static com.intellij.lang.jsgraphql.types.Assert.assertNotNull;
import static com.intellij.lang.jsgraphql.types.Assert.assertValidName;
import static com.intellij.lang.jsgraphql.types.schema.GraphqlTypeComparators.sortTypes;
import static com.intellij.lang.jsgraphql.types.util.FpKit.getByName;
import static com.intellij.lang.jsgraphql.types.util.FpKit.valuesToList;
import static java.lang.String.format;
import static java.util.Collections.emptyList;

/**
 * In graphql, an interface is an abstract type that defines the set of fields that a type must include to
 * implement that interface.
 * <p>
 * At runtime a {@link com.intellij.lang.jsgraphql.types.schema.TypeResolver} is used to take an interface object value and decide what {@link com.intellij.lang.jsgraphql.types.schema.GraphQLObjectType}
 * represents this interface type.
 * <p>
 * See http://graphql.org/learn/schema/#interfaces for more details on the concept.
 */
@PublicApi
public class GraphQLInterfaceType
  implements GraphQLNamedType, GraphQLCompositeType, GraphQLUnmodifiedType, GraphQLNullableType, GraphQLDirectiveContainer,
             GraphQLImplementingType {

  private final String name;
  private final String description;
  private final Map<String, GraphQLFieldDefinition> fieldDefinitionsByName = new LinkedHashMap<>();
  private final TypeResolver typeResolver;
  private final InterfaceTypeDefinition definition;
  private final ImmutableList<InterfaceTypeExtensionDefinition> extensionDefinitions;
  private final DirectivesUtil.DirectivesHolder directives;

  private final ImmutableList<GraphQLNamedOutputType> originalInterfaces;
  private final Comparator<? super GraphQLSchemaElement> interfaceComparator;
  private ImmutableList<GraphQLNamedOutputType> replacedInterfaces;


  public static final String CHILD_FIELD_DEFINITIONS = "fieldDefinitions";
  public static final String CHILD_DIRECTIVES = "directives";
  public static final String CHILD_INTERFACES = "interfaces";


  @Internal
  @Deprecated(forRemoval = true)
  public GraphQLInterfaceType(String name,
                              String description,
                              List<GraphQLFieldDefinition> fieldDefinitions,
                              TypeResolver typeResolver, List<GraphQLDirective> directives,
                              InterfaceTypeDefinition definition,
                              List<InterfaceTypeExtensionDefinition> extensionDefinitions,
                              List<GraphQLNamedOutputType> interfaces,
                              Comparator<? super GraphQLSchemaElement> interfaceComparator) {
    assertValidName(name);
    assertNotNull(fieldDefinitions, () -> "fieldDefinitions can't null");
    assertNotNull(directives, () -> "directives cannot be null");

    this.name = name;
    this.description = description;
    this.typeResolver = typeResolver;
    this.definition = definition;
    this.interfaceComparator = interfaceComparator;
    this.originalInterfaces = ImmutableList.copyOf(sortTypes(interfaceComparator, interfaces));
    this.extensionDefinitions = ImmutableList.copyOf(extensionDefinitions);
    this.directives = new DirectivesUtil.DirectivesHolder(directives);
    buildDefinitionMap(fieldDefinitions);
  }

  private void buildDefinitionMap(List<GraphQLFieldDefinition> fieldDefinitions) {
    for (GraphQLFieldDefinition fieldDefinition : fieldDefinitions) {
      String name = fieldDefinition.getName();
      if (fieldDefinitionsByName.containsKey(name)) {
        throw new AssertException(format("Duplicated definition for field '%s' in interface '%s'", name, this.name));
      }
      fieldDefinitionsByName.put(name, fieldDefinition);
    }
  }

  @Override
  public GraphQLFieldDefinition getFieldDefinition(String name) {
    return fieldDefinitionsByName.get(name);
  }


  @Override
  public List<GraphQLFieldDefinition> getFieldDefinitions() {
    return ImmutableList.copyOf(fieldDefinitionsByName.values());
  }

  @Override
  public String getName() {
    return name;
  }

  @Override
  public String getDescription() {
    return description;
  }

  // to be removed in a future version when all code is in the code registry
  TypeResolver getTypeResolver() {
    return typeResolver;
  }

  @Override
  public InterfaceTypeDefinition getDefinition() {
    return definition;
  }

  public List<InterfaceTypeExtensionDefinition> getExtensionDefinitions() {
    return extensionDefinitions;
  }

  @Override
  public List<GraphQLDirective> getDirectives() {
    return directives.getDirectives();
  }

  @Override
  public Map<String, GraphQLDirective> getDirectivesByName() {
    return directives.getDirectivesByName();
  }

  @Override
  public Map<String, List<GraphQLDirective>> getAllDirectivesByName() {
    return directives.getAllDirectivesByName();
  }

  @Override
  public GraphQLDirective getDirective(String directiveName) {
    return directives.getDirective(directiveName);
  }

  @Override
  public String toString() {
    return "GraphQLInterfaceType{" +
           "name='" + name + '\'' +
           ", description='" + description + '\'' +
           ", fieldDefinitionsByName=" + fieldDefinitionsByName.keySet() +
           ", typeResolver=" + typeResolver +
           '}';
  }

  /**
   * This helps you transform the current GraphQLInterfaceType into another one by starting a builder with all
   * the current values and allows you to transform it how you want.
   *
   * @param builderConsumer the consumer code that will be given a builder to transform
   * @return a new object based on calling build on that builder
   */
  public GraphQLInterfaceType transform(Consumer<Builder> builderConsumer) {
    Builder builder = newInterface(this);
    builderConsumer.accept(builder);
    return builder.build();
  }

  @Override
  public TraversalControl accept(TraverserContext<GraphQLSchemaElement> context, GraphQLTypeVisitor visitor) {
    return visitor.visitGraphQLInterfaceType(this, context);
  }

  @Override
  public List<GraphQLSchemaElement> getChildren() {
    List<GraphQLSchemaElement> children = new ArrayList<>(fieldDefinitionsByName.values());
    children.addAll(directives.getDirectives());
    children.addAll(getInterfaces());
    return children;
  }

  @Override
  public SchemaElementChildrenContainer getChildrenWithTypeReferences() {
    return SchemaElementChildrenContainer.newSchemaElementChildrenContainer()
      .children(CHILD_FIELD_DEFINITIONS, fieldDefinitionsByName.values())
      .children(CHILD_DIRECTIVES, directives.getDirectives())
      .children(CHILD_INTERFACES, originalInterfaces)
      .build();
  }

  @Override
  public GraphQLInterfaceType withNewChildren(SchemaElementChildrenContainer newChildren) {
    return transform(builder ->
                       builder.replaceDirectives(newChildren.getChildren(CHILD_DIRECTIVES))
                         .replaceFields(newChildren.getChildren(CHILD_FIELD_DEFINITIONS))
                         .replaceInterfaces(newChildren.getChildren(CHILD_INTERFACES))
    );
  }

  @Override
  public List<GraphQLNamedOutputType> getInterfaces() {
    if (replacedInterfaces != null) {
      return ImmutableList.copyOf(replacedInterfaces);
    }
    return ImmutableList.copyOf(originalInterfaces);
  }

  void replaceInterfaces(List<GraphQLNamedOutputType> interfaces) {
    this.replacedInterfaces = ImmutableList.copyOf(sortTypes(interfaceComparator, interfaces));
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public final boolean equals(Object o) {
    return super.equals(o);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public final int hashCode() {
    return super.hashCode();
  }


  public static Builder newInterface() {
    return new Builder();
  }

  public static Builder newInterface(GraphQLInterfaceType existing) {
    return new Builder(existing);
  }


  @PublicApi
  public static class Builder extends GraphqlTypeBuilder {
    private TypeResolver typeResolver;
    private InterfaceTypeDefinition definition;
    private List<InterfaceTypeExtensionDefinition> extensionDefinitions = emptyList();
    private final Map<String, GraphQLFieldDefinition> fields = new LinkedHashMap<>();
    private final List<GraphQLDirective> directives = new ArrayList<>();
    private final Map<String, GraphQLNamedOutputType> interfaces = new LinkedHashMap<>();

    public Builder() {
    }

    public Builder(GraphQLInterfaceType existing) {
      this.name = existing.getName();
      this.description = existing.getDescription();
      this.typeResolver = existing.getTypeResolver();
      this.definition = existing.getDefinition();
      this.extensionDefinitions = existing.getExtensionDefinitions();
      this.fields.putAll(getByName(existing.getFieldDefinitions(), GraphQLFieldDefinition::getName));
      this.interfaces.putAll(getByName(existing.originalInterfaces, GraphQLNamedType::getName));
      DirectivesUtil.enforceAddAll(this.directives, existing.getDirectives());
    }

    @Override
    public Builder name(String name) {
      super.name(name);
      return this;
    }

    @Override
    public Builder description(String description) {
      super.description(description);
      return this;
    }

    @Override
    public Builder comparatorRegistry(GraphqlTypeComparatorRegistry comparatorRegistry) {
      super.comparatorRegistry(comparatorRegistry);
      return this;
    }

    public Builder definition(InterfaceTypeDefinition definition) {
      this.definition = definition;
      return this;
    }

    public Builder extensionDefinitions(List<InterfaceTypeExtensionDefinition> extensionDefinitions) {
      this.extensionDefinitions = extensionDefinitions;
      return this;
    }

    public Builder field(GraphQLFieldDefinition fieldDefinition) {
      assertNotNull(fieldDefinition, () -> "fieldDefinition can't be null");
      this.fields.put(fieldDefinition.getName(), fieldDefinition);
      return this;
    }

    /**
     * Take a field builder in a function definition and apply. Can be used in a jdk8 lambda
     * e.g.:
     * <pre>
     *     {@code
     *      field(f -> f.name("fieldName"))
     *     }
     * </pre>
     *
     * @param builderFunction a supplier for the builder impl
     * @return this
     */
    public Builder field(UnaryOperator<GraphQLFieldDefinition.Builder> builderFunction) {
      assertNotNull(builderFunction, () -> "builderFunction can't be null");
      GraphQLFieldDefinition.Builder builder = GraphQLFieldDefinition.newFieldDefinition();
      builder = builderFunction.apply(builder);
      return field(builder);
    }

    /**
     * Same effect as the field(GraphQLFieldDefinition). Builder.build() is called
     * from within
     *
     * @param builder an un-built/incomplete GraphQLFieldDefinition
     * @return this
     */
    public Builder field(GraphQLFieldDefinition.Builder builder) {
      return field(builder.build());
    }

    public Builder fields(List<GraphQLFieldDefinition> fieldDefinitions) {
      assertNotNull(fieldDefinitions, () -> "fieldDefinitions can't be null");
      fieldDefinitions.forEach(this::field);
      return this;
    }

    public Builder replaceFields(List<GraphQLFieldDefinition> fieldDefinitions) {
      assertNotNull(fieldDefinitions, () -> "fieldDefinitions can't be null");
      this.fields.clear();
      fieldDefinitions.forEach(this::field);
      return this;
    }

    public boolean hasField(String fieldName) {
      return fields.containsKey(fieldName);
    }

    /**
     * This is used to clear all the fields in the builder so far.
     *
     * @return the builder
     */
    public Builder clearFields() {
      fields.clear();
      return this;
    }


    @Deprecated(forRemoval = true)
    public Builder typeResolver(TypeResolver typeResolver) {
      this.typeResolver = typeResolver;
      return this;
    }

    public Builder withDirectives(GraphQLDirective... directives) {
      assertNotNull(directives, () -> "directives can't be null");
      this.directives.clear();
      for (GraphQLDirective directive : directives) {
        withDirective(directive);
      }
      return this;
    }

    public Builder withDirective(GraphQLDirective directive) {
      assertNotNull(directive, () -> "directive can't be null");
      DirectivesUtil.enforceAdd(this.directives, directive);
      return this;
    }

    public Builder replaceDirectives(List<GraphQLDirective> directives) {
      assertNotNull(directives, () -> "directive can't be null");
      this.directives.clear();
      DirectivesUtil.enforceAddAll(this.directives, directives);
      return this;
    }

    public Builder withDirective(GraphQLDirective.Builder builder) {
      return withDirective(builder.build());
    }

    /**
     * This is used to clear all the directives in the builder so far.
     *
     * @return the builder
     */
    public Builder clearDirectives() {
      directives.clear();
      return this;
    }

    public Builder withInterface(GraphQLInterfaceType interfaceType) {
      assertNotNull(interfaceType, () -> "interfaceType can't be null");
      this.interfaces.put(interfaceType.getName(), interfaceType);
      return this;
    }

    public Builder replaceInterfaces(List<GraphQLInterfaceType> interfaces) {
      assertNotNull(interfaces, () -> "interfaces can't be null");
      this.interfaces.clear();
      for (GraphQLInterfaceType interfaceType : interfaces) {
        this.interfaces.put(interfaceType.getName(), interfaceType);
      }
      return this;
    }

    public Builder withInterface(GraphQLTypeReference reference) {
      assertNotNull(reference, () -> "reference can't be null");
      this.interfaces.put(reference.getName(), reference);
      return this;
    }

    public Builder withInterfaces(GraphQLInterfaceType... interfaceType) {
      for (GraphQLInterfaceType type : interfaceType) {
        withInterface(type);
      }
      return this;
    }


    public GraphQLInterfaceType build() {
      return new GraphQLInterfaceType(
        name,
        description,
        sort(fields, GraphQLInterfaceType.class, GraphQLFieldDefinition.class),
        typeResolver,
        sort(directives, GraphQLInterfaceType.class, GraphQLDirective.class),
        definition,
        extensionDefinitions,
        valuesToList(interfaces),
        getComparator(GraphQLInterfaceType.class, GraphQLInterfaceType.class)
      );
    }
  }
}
